/**
 * @author richardo2016@gmail.com
 * @email richardo2016
 * @create date 2021-04-11 19:01:26
 * @modify date 2021-04-11 19:01:26
 *
 * @desc fibjs's Isolate implementation
 */

#include "object.h"
#include "options.h"
#include "ifs/v8.h"
#include "ifs/global.h"
#include "SecureContext.h"
#include "HttpClient.h"
#include "SandBox.h"
#include "TTYStream.h"
#include "EventEmitter.h"
#include "PerformanceObserver.h"
#include "Fiber.h"

using namespace v8;

namespace fibjs {

void init_process_ipc(Isolate* isolate);

static exlib::LockedList<Isolate> s_isolates;
static exlib::atomic s_iso_id;
static exlib::atomic s_iso_ref;

Isolate::SnapshotJsScope::SnapshotJsScope(Isolate* cur)
    : m_isolate((cur ? cur : Isolate::current()))
{
    m_fb = JSFiber::current();
    V8FrameInfo _fi = save_fi(m_isolate->m_isolate);

    m_fb->m_c_entry_fp_ = _fi.entry_fp;
    m_fb->m_handler_ = _fi.handle;

    m_isolate->RunMicrotasks();
}

Isolate::SnapshotJsScope::~SnapshotJsScope()
{
    if (m_fb->m_termed && !m_isolate->m_isolate->IsExecutionTerminating())
        m_isolate->m_isolate->TerminateExecution();
}

bool Isolate::SnapshotJsScope::is_terminating()
{
    return m_fb->m_termed;
}

static void fb_GCCallback(v8::Isolate* js_isolate, v8::GCType type, v8::GCCallbackFlags flags)
{
    Isolate* isolate = Isolate::current(js_isolate);
    exlib::linkitem* p;
    exlib::List<exlib::linkitem> freelist;

    while (true) {
        isolate->m_weakLock.lock();
        isolate->m_weak.getList(freelist);
        isolate->m_weakLock.unlock();

        if (freelist.empty())
            break;

        while ((p = freelist.getHead()) != 0)
            object_base::gc_delete(p);
    }
}

static void FiberProcIsolate(void* p)
{
    Isolate* isolate = (Isolate*)p;
    Runtime rtForThread(isolate);

    isolate->init();
    JSFiber::FiberProcRunJavascript(p);
}

void Isolate::sync(std::function<int(void)> func)
{
    class SyncFunc : public AsyncEvent {
    public:
        SyncFunc(std::function<int(void)> func)
            : m_func(func)
        {
        }

        virtual result_t js_invoke()
        {
            result_t hr = m_func();
            delete this;
            return hr;
        }

    private:
        std::function<int(void)> m_func;
    };

    post_task(new SyncFunc(func));
}

class ShellArrayBufferAllocator : public v8::ArrayBuffer::Allocator {
public:
    virtual void* Allocate(size_t length)
    {
        void* data = AllocateUninitialized(length);
        return data == NULL ? data : memset(data, 0, length);
    }

    virtual void* AllocateUninitialized(size_t length)
    {
        return ::operator new(length);
    }

    virtual void Free(void* data, size_t)
    {
        ::operator delete(data);
    }
};

Isolate::Isolate(exlib::string jsFilename, exlib::string jsCode)
    : m_id((int32_t)s_iso_id.inc())
    , m_hr(0)
    , m_intask(false)
    , m_ipc_mode(0)
    , m_test(NULL)
    , m_currentFibers(0)
    , m_idleFibers(0)
    , m_fid(1)
    , m_flake_tm(0)
    , m_flake_host(0)
    , m_flake_count(0)
    , m_console_colored(true)
    , m_defaultMaxListeners(10)
    , m_exitCode(0)
    , m_enable_FileSystem(true)
    , m_safe_buffer(false)
    , m_file_cache(1024, 30, 30)
    , m_realpath_cache(1024, 30, 30)
{
    s_isolates.putTail(this);

    if (jsCode.empty())
        m_fname = jsFilename;
    else
        m_jsCode = jsCode;

    static v8::Isolate::CreateParams create_params;
    static ShellArrayBufferAllocator array_buffer_allocator;
    static v8::StartupData blob;

    if (blob.data == NULL) {
        v8::SnapshotCreator creator;
        v8::Isolate* isolate = creator.GetIsolate();
        {
            v8::HandleScope handle_scope(isolate);
            v8::Local<v8::Context> context = v8::Context::New(isolate);
            creator.SetDefaultContext(context);
        }
        blob = creator.CreateBlob(v8::SnapshotCreator::FunctionCodeHandling::kClear);
    }

    create_params.snapshot_blob = &blob;
    create_params.array_buffer_allocator = &array_buffer_allocator;

    m_isolate = v8::Isolate::New(create_params);
    m_isolate->SetData(0, this);
    m_isolate->AddGCEpilogueCallback(fb_GCCallback, v8::kGCTypeMarkSweepCompact);

    m_currentFibers++;
    m_idleFibers++;

    SecureContext* ctx = new SecureContext();
    ctx->SetRootCerts();
    m_ctx = ctx;

    m_httpclient = new HttpClient(m_ctx);

    exlib::Service::CreateFiber(FiberProcIsolate, this, stack_size * 1024, "JSFiber");
}

Isolate* Isolate::current()
{
    Runtime* rt = Runtime::current();
    if (rt == NULL)
        return NULL;

    return rt->isolate();
}

Isolate* Isolate::safe_current()
{
    Runtime* rt = Runtime::current();
    if (rt == NULL)
        return NULL;

    return rt->safe_isolate();
}

Isolate* Isolate::main()
{
    return s_isolates.head();
}

static void _PromiseRejectCallback(v8::PromiseRejectMessage data)
{
    Runtime* rt = Runtime::current();
    Isolate* isolate = rt->isolate();
    v8::PromiseRejectEvent e = data.GetEvent();

    v8::Local<v8::Context> _context = isolate->context();
    v8::Local<v8::Array> _promise_error;

    if (rt->m_promise_error.IsEmpty()) {
        _promise_error = v8::Array::New(isolate->m_isolate);
        rt->m_promise_error.Reset(isolate->m_isolate, _promise_error);
    } else
        _promise_error = rt->m_promise_error.Get(isolate->m_isolate);

    if (e == v8::kPromiseRejectWithNoHandler) {
        v8::Local<v8::Array> o = v8::Array::New(isolate->m_isolate);
        o->Set(_context, 0, data.GetPromise()).IsJust();
        o->Set(_context, 1, data.GetValue()).IsJust();
        _promise_error->Set(_context, rt->m_promise_error_no++, o).IsJust();
    } else if (e == v8::kPromiseHandlerAddedAfterReject) {
        v8::Local<v8::Promise> _promise = data.GetPromise();
        if (!_promise.IsEmpty()) {
            JSArray ks = _promise_error->GetPropertyNames(_context);
            int32_t len = ks->Length();

            for (int32_t i = 0; i < len; i++) {
                JSValue k = ks->Get(_context, i);
                JSValue v = _promise_error->Get(_context, k);

                v8::Local<v8::Array> o = v8::Local<v8::Array>::Cast(v);
                v = o->Get(_context, 0);

                bool e = _promise->Equals(_context, v).FromMaybe(false);
                if (e) {
                    _promise_error->Delete(_context, k).IsJust();
                }
            }
        }
    }
}

// Promise Hook callback for AsyncLocalStorage context propagation
static void PromiseHookCallback(v8::PromiseHookType type, v8::Local<v8::Promise> promise,
    v8::Local<v8::Value> parent)
{
    Isolate* isolate = Isolate::current(promise->GetIsolate());
    if (!isolate || isolate->m_async_context_symbol.IsEmpty())
        return;

    v8::Local<v8::Context> context = isolate->m_isolate->GetCurrentContext();
    if (context.IsEmpty())
        return;

    v8::Local<v8::Private> symbol = isolate->m_async_context_symbol.Get(isolate->m_isolate);
    JSFiber* fb = JSFiber::current();

    switch (type) {
    case v8::PromiseHookType::kInit: {
        // When a new promise is created, capture the current async context from fiber.
        // We only capture from fiber, NOT from parent promise. Parent inheritance
        // causes issues with await: the internal promise created by V8 for await
        // inherits the awaited promise's context, which incorrectly restores that
        // context after await completes.
        if (fb && !fb->m_async_ctx.IsEmpty()) {
            v8::Local<v8::Value> ctx = fb->m_async_ctx.Get(isolate->m_isolate);
            if (!ctx.IsEmpty() && ctx->IsMap())
                promise->SetPrivate(context, symbol, ctx).FromJust();
        }
        break;
    }
    case v8::PromiseHookType::kBefore: {
        // Before promise callback executes, restore promise's context to fiber.
        // In fibjs, each microtask runs in an independent fiber, so we don't need
        // to save/restore prior context - the fiber will be destroyed after callback.
        if (fb) {
            v8::MaybeLocal<v8::Value> maybeCtx = promise->GetPrivate(context, symbol);
            if (!maybeCtx.IsEmpty()) {
                v8::Local<v8::Value> ctx = maybeCtx.ToLocalChecked();
                if (!ctx.IsEmpty() && !ctx->IsUndefined() && ctx->IsMap())
                    fb->m_async_ctx.Reset(isolate->m_isolate, ctx);
                else
                    fb->m_async_ctx.Reset();
            } else {
                fb->m_async_ctx.Reset();
            }
        }
        break;
    }
    case v8::PromiseHookType::kAfter:
        // No action needed - fiber will be destroyed after callback
        break;
    case v8::PromiseHookType::kResolve:
        break;
    }
}

void Isolate::init()
{
    v8::Locker locker(m_isolate);
    v8::Isolate::Scope isolate_scope(m_isolate);
    v8::HandleScope handle_scope(m_isolate);

    init_global_template();

    v8::Local<v8::Context> _context = v8::Context::New(m_isolate, nullptr, m_global_template.Get(m_isolate));
    m_context.Reset(m_isolate, _context);

    v8::Context::Scope context_scope(_context);

    Fiber_base::class_info().getModule(this);

    JSFiber::EnterJsScope s;

    if (g_cov && m_id == 1)
        beginCoverage(m_isolate);

    _context->SetEmbedderData(kObjectPrototype, v8::Object::New(m_isolate)->GetPrototype());
    _context->SetEmbedderData(kSandboxObject, global_base::class_info().getModule(this));

    m_isolate->SetPromiseRejectCallback(_PromiseRejectCallback);
    m_isolate->SetHostImportModuleDynamicallyCallback(SandBox::ImportModuleDynamically);
    m_isolate->SetHostInitializeImportMetaObjectCallback(SandBox::ImportMetaObjectCallback);

    m_isolate->SetMicrotasksPolicy(v8::MicrotasksPolicy::kExplicit);

    // Initialize Promise Hook for AsyncLocalStorage context propagation
    m_async_context_symbol.Reset(m_isolate, v8::Private::New(m_isolate, NewString("asyncContext")));
    m_isolate->SetPromiseHook(PromiseHookCallback);

    init_process_ipc(this);
}

void Isolate::Ref()
{
    s_iso_ref.inc();
}

void Isolate::Unref(int32_t hr)
{
    if (s_iso_ref.dec() == 0) {
        Isolate* isolate = s_isolates.head();
        isolate->m_hr = hr;

        isolate->sync([isolate]() -> int {
            v8::HandleScope handle_scope(isolate->m_isolate);
            JSFiber::EnterJsScope s;
            JSTrigger t(isolate->m_isolate, process_base::class_info().getModule(isolate));
            v8::Local<v8::Value> code = v8::Number::New(isolate->m_isolate, isolate->m_exitCode);
            bool r;

            t._emit("beforeExit", &code, 1, r);
            if (s_iso_ref == 1) {
                if (isolate->m_hr >= 0)
                    process_base::exit();
                else
                    process_base::exit(1);
            }

            return 0;
        });
    }
}

void Isolate::start_profiler()
{
    if (g_prof) {
        char name[32];
        obj_ptr<Timer_base> tm;
        snprintf(name, sizeof(name), "fibjs-%08x.log", (uint32_t)(intptr_t)this);
        v8_base::start(name, -1, g_prof_interval, tm);
    }
}
void InvokeApiInterruptCallbacks(v8::Isolate* isolate);
void Isolate::RequestInterrupt(v8::InterruptCallback callback, void* data)
{
    m_isolate->RequestInterrupt(callback, data);
    if (m_has_timer.CompareAndSwap(0, 1) == 0)
        sync([this]() {
            JSFiber::EnterJsScope s;
            m_has_timer = 0;
            InvokeApiInterruptCallbacks(m_isolate);
            return 0;
        });
}

void Isolate::get_stdin(obj_ptr<Stream_base>& retVal)
{
    if (!m_stdin) {
        int32_t fd = _fileno(stdin);
        if (_is_ctx_atty(fd))
            m_stdin = new TTYInputStream(fd);
        else
            m_stdin = new UVStream(fd);
    }

    retVal = m_stdin;
}

void Isolate::get_stdout(obj_ptr<Stream_base>& retVal)
{
    if (!m_stdout) {
        int32_t fd = _fileno(stdout);
        if (_is_ctx_atty(fd))
            m_stdout = new TTYOutputStream(fd);
        else
            m_stdout = new UVStream(fd);
    }

    retVal = this->m_stdout;
}

void Isolate::get_stderr(obj_ptr<Stream_base>& retVal)
{
    if (!m_stderr) {
        int32_t fd = _fileno(stderr);
        if (_is_ctx_atty(fd))
            m_stderr = new TTYOutputStream(fd);
        else
            m_stderr = new UVStream(fd);
    }

    retVal = m_stderr;
}

} // namespace fibjs